<!DOCTYPE html>
<html lang="zh-CN">
<head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
    <title>
        
            [翻译]Common CRUD Design in Go
        
    </title>
    <link rel="icon" href="/img/favicon.png"/>
    
<link rel="stylesheet" href="/css/style.css">

    
<link rel="stylesheet" href="/css/font-awesome.min.css">

    <!-- 
<link rel="stylesheet" href="/css/gitment.css">
 -->
    
<link rel="stylesheet" href="/css/hljs.min.css">

    
<script src="/js/hljs.min.js"></script>
  
    <script>
        var _hmt = _hmt || [];
        (function() {
          var hm = document.createElement("script");
          hm.src = "https://hm.baidu.com/hm.js?8ad9e7d37411eee585b4bd7194806394";
          var s = document.getElementsByTagName("script")[0]; 
          s.parentNode.insertBefore(hm, s);
        })();
    </script>  
    <!-- 
<script src="/js/gitment.browser.js"></script>
   -->
    <link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css">
    <script src="https://unpkg.com/gitalk/dist/gitalk.min.js"></script>
<meta name="generator" content="Hexo 5.3.0"><link rel="alternate" href="/atom.xml" title="Nick" type="application/atom+xml">
</head>
<body>
    <header class="header" id="header">
    <h1>
        <a class="title" href="/">
            Nick
        </a>
    </h1>
    <h2>
        <a class="motto">
            天地间的怪事莫过于侦察别人的一些和自己绝不相干的事了
        </a>
    </h2>
    <nav class="navbar">
        <ul class="menu">
            
            
                <li class="menu-item">
                    <a href="/" class="menu-item-link">
                        首页
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a href="/archives" class="menu-item-link">
                        归档
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a target="_blank" rel="noopener" href="https://github.com/StoneQI" class="menu-item-link">
                        Github
                    </a>
                </li>
            
                
            
                <li class="menu-item">
                    <a href="/atom.xml" class="menu-item-link">
                        RSS
                    </a>
                </li>
            
                
            
                
            
                <li class="menu-item">
                    <a href="/category-archive" class="menu-item-link">
                        目录
                    </a>
                </li>
            
                
                
                <li class="menu-item">
                    <a class="menu-item-link search">
                        Search                   
                        <i class="fa fa-long-arrow-right search-icon" aria-hidden="true"></i>
                    </a>
                        <input placeholder="Search..." class="search-input" style="display:none;border:none!important;" onkeydown="onEnter(event)" onkeypress="onEnter(event)"></input>
                </li>
                
        </ul>
    </nav>
</header>
    <main class="main">
        <article class="post">
            <h1>
                <a class="title" href="/2021/01/24/tr-common-crud-design-in-go/"> 
                    [翻译]Common CRUD Design in Go 
                </a>
            </h1>
            <div class="meta">
                <a class="date"> 
                    <i class="fa fa-calendar" aria-hidden="true"></i>                    
                    2021-01-24   
                </a>
                
                <a class="category">
                    <i class="fa fa-th" aria-hidden="true"></i>  
                </a>
               
                <ul class="category-list"><li class="category-list-item"><a class="category-list-link" href="/categories/go/">go</a></li></ul>
                
                <a class="tag">
                    <i class="fa fa-tags" aria-hidden="true"></i>  
                </a>
                
                    <ul class="tag-list" itemprop="keywords"><li class="tag-list-item"><a class="tag-list-link" href="/tags/CRUD/" rel="tag">CRUD</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/go/" rel="tag">go</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/%E7%BF%BB%E8%AF%91/" rel="tag">翻译</a></li></ul>
            </div>
<div class="toc">
  <ol class="toc-list"><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1"><span class="toc-list-text">接口设计</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E4%BA%8B%E5%8A%A1%E8%BE%B9%E7%95%8C"><span class="toc-list-text">事务边界</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E9%80%9A%E8%BF%87-Context-%E9%AA%8C%E8%AF%81%E5%AE%89%E5%85%A8"><span class="toc-list-text">通过 Context 验证安全</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E6%9F%A5%E6%89%BE%E5%8D%95%E4%B8%AA%E5%AF%B9%E8%B1%A1"><span class="toc-list-text">查找单个对象</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E4%B8%8D%E8%A6%81%E8%BF%94%E5%9B%9E%E4%B8%A4%E4%B8%AA-nil"><span class="toc-list-text">不要返回两个 nil</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E9%80%89%E6%8B%A9%E8%BF%94%E5%9B%9E%E7%9A%84%E6%95%B0%E6%8D%AE"><span class="toc-list-text">选择返回的数据</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E6%9F%A5%E8%AF%A2%E5%A4%9A%E4%B8%AA%E6%95%B0%E6%8D%AE"><span class="toc-list-text">查询多个数据</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E8%BF%87%E6%BB%A4%E7%BB%93%E6%9E%9C"><span class="toc-list-text">过滤结果</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E7%BB%93%E6%9E%9C%E5%88%87%E7%89%87-amp-%E7%BB%93%E6%9E%9C%E7%BB%9F%E8%AE%A1"><span class="toc-list-text">结果切片 &amp; 结果统计</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E6%8E%92%E5%BA%8F%E7%BB%93%E6%9E%9C%E9%9B%86"><span class="toc-list-text">排序结果集</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E6%96%B0%E5%BB%BADial"><span class="toc-list-text">新建Dial</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E4%BB%A5%E4%BA%8B%E5%8A%A1%E7%9A%84%E6%96%B9%E5%BC%8F%E6%9E%84%E5%BB%BA%E5%AF%B9%E8%B1%A1%E5%9B%BE"><span class="toc-list-text">以事务的方式构建对象图</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E6%9B%B4%E6%96%B0Dial"><span class="toc-list-text">更新Dial</span></a><ol class="toc-list-child"><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E8%BF%94%E5%9B%9E%E9%94%99%E8%AF%AF%E7%9A%84%E8%A1%A8%E7%9B%98"><span class="toc-list-text">返回错误的表盘</span></a></li><li class="toc-list-item toc-list-level-2"><a class="toc-list-link" href="#%E6%89%B9%E9%87%8F%E6%9B%B4%E6%96%B0"><span class="toc-list-text">批量更新</span></a></li></ol></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E5%88%A0%E9%99%A4-Dial"><span class="toc-list-text">删除 Dial</span></a></li><li class="toc-list-item toc-list-level-1"><a class="toc-list-link" href="#%E7%BB%93%E8%AE%BA"><span class="toc-list-text">结论</span></a></li></ol>
</div>
            <div class="content">
                <blockquote>
<p>原文由 Ben Johnson 发表在 <a target="_blank" rel="noopener" href="https://www.gobeyond.dev/">gobeyond</a>，推荐观看原文<a target="_blank" rel="noopener" href="https://www.gobeyond.dev/crud/">文章链接</a></p>
</blockquote>
<p>增删改查（CRUD）是一个技术产品的基础部分，做过应用开发的人应该很熟悉它。</p>
<p>开发CRUD应用时，大多数编程语言会有框架提供一个固定的开发架构，例如PHP的Yii2、Java的SSH。但是总所周知Go社区是反框架的。因此，我们需要设计自己的CRUD架构。</p>
<p>在一年的Go应用开发经验后，我发现了一套通用的CRUD设计模式，能满足大多数不同的项目的要求。我将以开发 <a target="_blank" rel="noopener" href="https://github.com/benbjohnson/wtf">WTF Dial</a> 项目为例进行说明。 项目的详细介绍<a target="_blank" rel="noopener" href="https://www.gobeyond.dev/wtf-dial/">参考链接</a>。</p>
<p>译者：<a target="_blank" rel="noopener" href="https://github.com/benbjohnson/wtf">WTF Dial(which they feel)</a> 项目提供一个界面，每个成员可输入对当前团队的糟糕程度(f-cked)。</p>
<a id="more"></a>
<h1 id="接口设计"><a href="#接口设计" class="headerlink" title="接口设计"></a>接口设计</h1><p>在 WTF Dial应用中，我们采用面向接口开发，定义软件提供的服务为一个接口。通过接口提供服务，我们底层可以使用不同的实现方法。在<code>dial.go</code>中，我们定义了如下<code>DialService</code> 接口。</p>
<pre><code class="go">// dial.go#L81-L122
type DialService interface &#123;
    FindDialByID(ctx context.Context, id int) (*Dial, error)
    
    FindDials(ctx context.Context, filter DialFilter) ([]*Dial, int, error)
    
    CreateDial(ctx context.Context, dial *Dial) error
    
    UpdateDial(ctx context.Context, id int, upd DialUpdate) (*Dial, error)
    
    DeleteDial(ctx context.Context, id int) error
&#125;
</code></pre>
<p>这个结构是我在应用程序中使用的几乎所有实体。它提供了一个简单的结构，但在大多数情况下它都足够灵活。</p>
<h2 id="事务边界"><a href="#事务边界" class="headerlink" title="事务边界"></a>事务边界</h2><p>我将我的服务定义视为一个黑盒子。因此，我很少向应用程序的其他部分公开事务等内部细节。虽然让服务的调用者组成单独的事务调用可能很诱人，但很少有必要这样做，而且通常会使应用程序复杂化。</p>
<p>译者注：我的理解为将一个服务接口看作一个事务调用，复杂的调用也组织成一个事务。</p>
<h2 id="通过-Context-验证安全"><a href="#通过-Context-验证安全" class="headerlink" title="通过 Context 验证安全"></a>通过 Context 验证安全</h2><p>在<code>WTF Dial</code>中，当请求到来时，将对user进行身份验证。然后验证通过过的user 将被添加到<a target="_blank" rel="noopener" href="https://github.com/benbjohnson/wtf/blob/e23f5f00e0f48f54bd751cc264ea85c094f7d466/context.go#L24-L27"><code>NewContextWithUser()</code></a>函数创建 <code>context.Context</code>中。这意味着我们的服务中的任何函数都可以通过ctx参数访问当前 user。</p>
<p>出于几个原因，强制授权被内置到服务实现中。首先，它确保验证在可能的最低级别执行，而不是委托给更高级别的抽象。当我们可以将安全检查直接嵌入到SQL查询中时，我们不太可能忘记安全检查。其次，将这些限制推到数据库层会更有效，因为它限制查询和返回的数据。</p>
<p>下面是<code>sqlite.findDials()</code>函数内的一个安全验证的示例，我们将限制用户仅能查询自己所属组的   <code>dial_id</code>:</p>
<pre><code class="go">// sqlite/dial.go#L306-L310
userID := wtf.UserIDFromContext(ctx)
where = append(where, `id IN (SELECT dial_id FROM dial_memberships dm WHERE dm.user_id = ?)`)
args = append(args, userID)
</code></pre>
<h1 id="查找单个对象"><a href="#查找单个对象" class="headerlink" title="查找单个对象"></a>查找单个对象</h1><p>根据主键查找对象是您将遇到的最常见任务之一。这里我们定义了一个根据其<code>id</code>获取<code>wtf.Dial</code>的函数:</p>
<pre><code class="go">FindDialByID(ctx context.Context, id int) (*Dial, error)
</code></pre>
<p>这个函数定义看似简单，但有重要的问题需要确定。如果<code>ID</code>不存在怎么办? 这种情况应该定义为一种 <code>error</code> 还是 <code>*Dial</code>为空？</p>
<h2 id="不要返回两个-nil"><a href="#不要返回两个-nil" class="headerlink" title="不要返回两个 nil"></a>不要返回两个 nil</h2><p>我看到的一个常见做法是，如果<code>ID</code>无法找到，开发人员将返回 <code>nil</code> Dial 和 <code>nil</code> error。然而，在这种情况下，用户希望调用函数得到一个特定的 <code>Dial</code>，而不是一个<code>nil</code>，因此<code>ID</code>未发现将是一个<code>err</code>。</p>
<p>在实践中，函数的调用者将执行一个简单的 <code>err != nil</code> 检查是否错误发生，但很容易忘记检查 <code>nil</code> Dial。这将导致您的程序 <code>panic</code>。</p>
<pre><code class="go">// Try to fetch the dial by ID but it doesn&#39;t exist.
dial, err := FindDialByID(ctx, 100)
if err != nil &#123;
    return err
&#125;
// 未检查 dial 是否为空
// 译者注：保证返回的数据是可用的，否者都返回一个error
// Oops! Panic here because dial is nil.
fmt.Printf(&quot;WTF Level: %d&quot;, dial.Value)

</code></pre>
<p>确保返回一个对象或一个错误，两者不能同时为<code>nil</code>.</p>
<h2 id="选择返回的数据"><a href="#选择返回的数据" class="headerlink" title="选择返回的数据"></a>选择返回的数据</h2><p>当返回我们的<code>Dial</code>对象时，调用者通常也需要其他的附加信息。<code>Dial</code>的用户是谁? 谁是 <code>Dial</code> 的其他成员?(即数据库中的一对一、一对多、多对多关系。例如学生和班级、老师和学生、老师和班级)。我们的数据是一个相互关联的图，所以我们需要定义返回的具体数据。</p>
<p>我们可以允许调用者使用额外的标记参数告诉服务需要那些参数，但这会增加应用程序的复杂性。相对来说，后台根据业务直接返回包含有用的相关数据更加容易。虽然这会招致额外的数据库调用或增加网络带宽，但这通常是一个很好的权衡，我们可以根据需要优化返回情况。</p>
<p>译者注：比如返回一篇博文时，通常会标签会经常使用，我们可以直接一起返回。但是评论可能很少使用，我们可以提供单独接口。</p>
<p>这里有个判定方法：我通常返回与主对象有父关系的相关数据。在Dial的例子中，它有一个我要附加的用户父对象。调用者几乎总是需要这些关系，因为它们为对象提供了上下文。</p>
<p><img src="/img/tr-common-crud-design-in-go/2021-01-24-18-48-53.png"></p>
<p>而返回子关系的数据会很容易地造成返回数据过多。但是。如果我知道子对象的数量是有限的，并且在查看父对象时它们几乎总是被获取，那么我就会包括子关系。在 <code>Dial</code> 的情况下，我们可以包括<code>Dial</code>的成员列表，因为这通常是有用的，我们永远不会有超过少数的成员。另一个很好的例子是返回一组带有电子商务订单的订单项。</p>
<p>译者注：更简单理解，如果数据存在经常被使用的一对一关系，可以一起返回。如果存在一对多则要仔细考虑是否经常使用到该关联数据。</p>
<h1 id="查询多个数据"><a href="#查询多个数据" class="headerlink" title="查询多个数据"></a>查询多个数据</h1><p>我们的下一个函数提供了一种通过各种过滤选项来搜索<code>Dial</code>数据的方法。获取一个<code>Dial</code>列表听起来类似于获取单个<code>Dial</code>，但有一些重要的区别。</p>
<pre><code class="go">FindDials(ctx context.Context, filter DialFilter) ([]*Dial, int, error)
</code></pre>
<p><code>*Dial</code> 和 <code>error</code> 同时返回nil是可以的。与<code>FindDialByID()</code>不同，它可以不返回<code>Dial</code> 并返回<code>nil</code> error(即没有错误发生，数据也为空)。调用者可能不知道是否有任何匹配的<code>Dial</code>(这就是他们搜索的原因)，所以不匹配任何<code>Dial</code>不是一个错误条件。</p>
<p>我们也不需要像查找单个<code>Dial</code>时那样担心<code>panic</code>，因为我们返回的是一个切片。切片上的大多数操作(<code>len(</code>)或<code>for in</code>)都可以在<code>nil slice</code>上正常工作。</p>
<pre><code class="go">
// Search for a list of all dials.
dials, _, err := FindDials(ctx, DialFilter&#123;&#125;)
if err != nil &#123;
    return err
&#125;

// 译者注：空切片除了取值大部分操作都不会发生panic，因此对切片取值操作前必须判断是否为空
// No panic this time. A nil slice of dials is ok.
fmt.Printf(&quot;You have %d dials.&quot;, len(dials))
// Returning a nil list and a nil error will not cause a panic
</code></pre>
<h2 id="过滤结果"><a href="#过滤结果" class="headerlink" title="过滤结果"></a>过滤结果</h2><p>在这个函数中，我们传入一个<code>filter</code>对象，而不是多个过滤参数。这允许我们添加额外的过滤器，而不会破坏未来的<code>API</code>兼容性。</p>
<pre><code class="go">// dial.go#L124-L133
// DialFilter represents a filter used by FindDials().
type DialFilter struct &#123;
    // Filtering fields.
    ID         *int    `json:&quot;id&quot;`
    InviteCode *string `json:&quot;inviteCode&quot;`

    // 限制结果的一个子集，通常用在分页中
    // Restrict to subset of range.
    Offset int `json:&quot;offset&quot;`
    Limit  int `json:&quot;limit&quot;`
&#125;
</code></pre>
<p>我们在过滤器结构中使用指针，这样我们就可以选择性地添加过滤属性(不需要过滤，置为 <code>nil</code> 即可)。我们设置的每个字段将进一步过滤查找返回的结果。</p>
<p><img src="/img/tr-common-crud-design-in-go/2021-01-24-19-17-18.png"></p>
<h2 id="结果切片-amp-结果统计"><a href="#结果切片-amp-结果统计" class="headerlink" title="结果切片 &amp; 结果统计"></a>结果切片 &amp; 结果统计</h2><p>上面 <code>DialFilter</code> 对象中的 <code>Offset</code> 和 <code>Limit</code> 字段可用于返回结果的子集，类似于 <code>SQL</code> 中的 <code>Offset</code> 和 <code>Limit</code> 子句。通常用作分页。</p>
<p>即使我们限制了返回的 <code>Dial</code> 数量，但是，我们仍需要知道匹配的 <code>Dial</code> 的总数。例如，分页中知道总数才能确定分多少页。为此，除了返回<code>[]*Dial</code>外，我们还返回一个<code>int</code>值，表示结果总数。</p>
<p>一些数据库允许我们在一个SQL查询中使用<code>COUNT(*) OVER()</code> 计算查询到数据总量。例如，如果我们搜索用户ID为100的<code>Dial</code>，并且我们将搜索限制为20条记录，我们仍然可以得到查询到的总数:</p>
<pre><code class="sql">-- 同时返回20 dials 和 dials 总量
SELECT id, name, COUNT(*) OVER()
FROM dials
WHERE user_id = 100
ORDER BY id
LIMIT 20
</code></pre>
<p>我们可以遍历得到结果集，并获得数据的总数，如下所示:</p>
<pre><code class="go">var dials []*Dial
// n 保存 总数
var n int
// rows 为 GO SQL查询返回的结果集
for rows.Next() &#123;
    var dial Dial
    if rows.Scan(&amp;dial.ID, &amp;dial.UserID, &amp;n); err != nil &#123;
        return err
    &#125;
    dials = append(dials, &amp;dial)
&#125;
// 查询集每一行都会返回n，但是值都是一样的。
</code></pre>
<h2 id="排序结果集"><a href="#排序结果集" class="headerlink" title="排序结果集"></a>排序结果集</h2><p>对于排序，不能允许用户按数据库中的任何列排序。因为大多数列不会被索引，所以查询会很慢。相反，我建议将一组固定的值映射到数据库中的列。例如，“name_asc”可以映射到 <code>ORDER BY name ASC</code>子句。</p>
<p>具体例子可见 WTF Dial中搜索会员后按修改时间排序</p>
<pre><code class="go">// sqlite/dial_membership.go#L164-L172
var sortBy string
// filter 为上文的过滤结构体
switch filter.SortBy &#123;
case &quot;updated_at_desc&quot;:
    sortBy = &quot;dm.updated_at DESC&quot;
default:
    sortBy = `dm.ID ASC`
&#125;
</code></pre>
<p>在这个代码片段中，我们检查 filter.SortBy 字段是否设置为预定义的排序顺序(“updated_at_desc”)。如果是，我们将其转换为一个SQL代码片段。否则，我们使用默认排序情况。可以保证只使用我们定义好的排序规则，用户不能自定义其他列上的排序，保证查找数据的高效。</p>
<h1 id="新建Dial"><a href="#新建Dial" class="headerlink" title="新建Dial"></a>新建<code>Dial</code></h1><p>To create a new user in our application, we have the following function:</p>
<p>为了在我们的应用程序中创建一个<code>Dial</code>，我们使用以下函数:</p>
<pre><code class="go">CreateDial(ctx context.Context, dial *Dial) error
</code></pre>
<p>这里我们传入我们想要创建的<code>Dial</code>对象。我们需要将新的拨号ID传回给调用者，以便更新主键<code>dial.ID</code>和由服务实现生成的任何其他字段(例如创建日期)。</p>
<p>如果您不想更新原始的Dial对象，也可以从函数返回一个单独的Dial对象。但是，我发现这种方法在实践中比较麻烦。</p>
<p>译者注：这里的含义是传入 CreateDial 的 <code>Dial</code> 对象的 <code>ID</code> 和 <code>UpdatedAt</code> 和 <code>CreatedAt</code> 等都为空。插入SQL执行后，获得对应的属性赋值给传入的<code>Dial</code>原对象。在业务层就能直接使用该<code>Dial</code>,而不用新返回一个<code>Dial</code>了。</p>
<h2 id="以事务的方式构建对象图"><a href="#以事务的方式构建对象图" class="headerlink" title="以事务的方式构建对象图"></a>以事务的方式构建对象图</h2><p>因为我们将事务边界限制为函数调用(一个函数调用为一个事务)，所以应该允许创建适当的嵌套对象。例如，我们可以接受附加到将在同一个事务中创建的<code>Dial</code>上的<code>DialMembership</code>对象列表。</p>
<pre><code class="go">svc.CreateDial(ctx, &amp;wtf.Dial&#123;
    UserID:      100,
    Name:        &quot;My Dial&quot;,
    Memberships: []*wtf.DialMembership&#123;
        &#123;
            User:  &amp;wtf.User&#123;Email:&quot;susy@que.com&quot;&#125;,
            Value: 50,
        &#125;,
        &#123;
            User:  &amp;wtf.User&#123;Email:&quot;john@doe.com&quot;&#125;,
            Value: 50,
        &#125;,
    &#125;,
&#125;)
// 在一个函数调用中创建包含多个 members 的一个 dial
// Creating a dial and with a multiple members in one call
</code></pre>
<h1 id="更新Dial"><a href="#更新Dial" class="headerlink" title="更新Dial"></a>更新<code>Dial</code></h1><p>对于更新已存在的用户，我们有以下函数:</p>
<pre><code class="go">UpdateDial(ctx context.Context, id int, upd DialUpdate) (*Dial, error)
</code></pre>
<p>这个函数使用<code>upd</code>中设置的字段值更新一个给定<code>ID</code>的<code>Dial</code>。并返回新更新的<code>Dial</code>。我们的更新类型为<code>DialUpdate</code>，其中包含我们允许更新的字段(一般来说,只有部分字段允许更新:</p>
<pre><code class="go">// DialUpdate represents a set of fields to update on a dial.
type DialUpdate struct &#123;
    Name *string `json:&quot;name&quot;`
&#125;
</code></pre>
<p>注意，Name字段是指针类型表明它是可选的。如果它没有被设置，那么它就不会被更新。我们的<code>DialUpdate</code>类型很简单，但是如果我们希望允许用户将<code>Dial</code>重新分配给其他人，我们可以想象添加一个<code>UserID</code>字段。这使得我们可以避免向我们的服务添加新接口<code>ReassignDial()</code>。</p>
<h2 id="返回错误的表盘"><a href="#返回错误的表盘" class="headerlink" title="返回错误的表盘"></a>返回错误的表盘</h2><p>与许多Go函数不同，<code>UpdateDial()</code>总是返回一个<code>Dial</code>对象，即使发生了错误。这是很有用的，因为用户通常希望看到如果出现验证错误，他们试图更新<code>Dial</code>的状态。对于基于web的应用程序来说每个HTTP请求都是无状态的，返回<code>Dial</code>这一点尤其重要。</p>
<h2 id="批量更新"><a href="#批量更新" class="headerlink" title="批量更新"></a>批量更新</h2><p><code>id</code>字段被有意地从<code>DialUpdate</code>类型中分离出来，这样我们也可以允许批量更新。例如，我们可以构建一个名为<code>UpdateDials()</code>的函数:</p>
<pre><code class="go">UpdateDials(ctx context.Context, ids []int, upd DialUpdate) ([]*Dial, error)
</code></pre>
<p>通过将函数更改为接受<code>id</code>列表，我们可以将其应用于所有id。同时，我们需要返回一个更新后的<code>Dial</code>列表。</p>
<h1 id="删除-Dial"><a href="#删除-Dial" class="headerlink" title="删除 Dial"></a>删除 <code>Dial</code></h1><p>老实说，关于删除的事没什么好说的。我们有一个简单的按主键删除的函数:</p>
<pre><code class="go">DeleteDial(ctx context.Context, id int) error
</code></pre>
<p>我们可以通过提供一个 <code>id</code> 切片来将其展开为一个批量删除:</p>
<pre><code class="go">DeleteDials(ctx context.Context, id []int) error
</code></pre>
<p>请确保执行授权限制，以确保用户不能删除其他用户的<code>Dial</code>。</p>
<h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>优化CRUD应用程序开发至关重要，因为它占据了大多数应用程序代码的大部分。我们已经看了一个构建Go CRUD函数的基本框架，它在灵活性和简单性之间取得了平衡。您需要调整这个框架，因为每个应用程序都有自己独特的需求，但希望它从一个坚实的基础开始。</p>
<p>如果您有<code>WTF Dial</code>问题，评论，或建议，请访问<a target="_blank" rel="noopener" href="https://github.com/benbjohnson/wtf/discussions">WTF Dial GitHub Discussion board</a>。</p>
<p>如果您对本文有问题，评论，或建议，请访问<a target="_blank" rel="noopener" href="https://github.com/StoneQI/StoneQI.github.io/discussions">GitHub Discussion board</a>,这是一个Github讨论版，使用它可以进行更方便的交流。</p>

            </div>
          
           
            <div class="copyright">
                <div class="name">
                    <a>本文作者:</a>
                    <a>Nick Yang</a>
                </div>
                <div class="link">
                    <a>本文链接:</a>
                    <a class="permalink" href="https://stoneqi.github.io/2021/01/24/tr-common-crud-design-in-go/">https://stoneqi.github.io/2021/01/24/tr-common-crud-design-in-go/</a>
                </div>
                <div class="license">
                    <a>版权声明:</a>
                    <a>本博客所有文章除特别声明外，均采用许可协议 CC BY-NC-SA 3.0 CN。转载请注明出处！</a>
                </div>
            </div>
            

          


</article>


<div class="tip">
<button class="tip-btn">
    打赏
</button>
<div class="tip-img">
<ul>
    
<li>
    <img src="/img/aipay.jpg"></img>
</li>

 
<li>
    <img src="/img/wechatpay.png"></img>
</li>

</ul>
</div>
</div>

<div class="more">

    <div class="prev">
   <a href="/2021/01/25/tr-packages-as-layers-not-groups/">  [翻译]Packages as layers, not groups</a>
    </div>

<div></div>

</div>

<div class="bdsharebuttonbox">
<a href="#" class="bds_weixin fa fa-weixin" data-cmd="weixin" title="分享到微信" style="color:#1cbd8f">
</a>
<a href="#" class="bds_tsina fa fa-weibo" data-cmd="tsina" title="分享到新浪微博" style="color:#ff6363">
</a>
<a href="#" class="bds_twi fa fa-twitter" data-cmd="twi" title="分享到Twitter" style="color:#00A7EB">
</a>
<a href="#" class="bds_fbook fa fa-facebook" data-cmd="fbook" title="分享到Facebook" style="color:#00A7EB">
</a>
</div>
<script>
window._bd_share_config={
    "common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"1","bdSize":"16"},
    "share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='../../../../../static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];
</script>
 
<!-- <div id="comments"></div> -->
<div id="gitalk-container"></div>

<script>
  //   var gitment = new Gitment({
  //   id: "Sun Jan 24 2021 20:38:13 GMT+0800",
  //   owner: "StoneQi",
  //   repo: "stoneqi.github.io",
  //   oauth: {
  //     client_id:"d9f4e8907ec2566c2307",
  //     client_secret: "2390fe6084c9227566e82de1755e3c3de4a13592",
  //   }
  // })
  // gitment.render('comments')

  const gitalk = new Gitalk({
  clientID: "d9f4e8907ec2566c2307",
  clientSecret: "2390fe6084c9227566e82de1755e3c3de4a13592",
  repo: "stoneqi.github.io",      // The repository of store comments,
  owner: "StoneQi",
  admin: ["StoneQi"],
  language: "zh-CN",
  id: "Sun Jan 24 2021 20:38:13 GMT+0800",      // Ensure uniqueness and length less than 50
  distractionFreeMode: false  // Facebook-like distraction free mode
})

gitalk.render('gitalk-container')
</script>
    </main>
    <a class="not-found">not found!</a>
    <div class="search-items">
    </div>
    <a href="#header" id="top" style="display:none">
        <i class="fa fa-sort-asc fa-2x"></i>
    </a>
    <footer class="footer">
    <div class="footer-copyright">©️2017
    <a href="//github.com/Vevlins/toki" class="link" target="_blank">Toki</a>  by Vevlins
    </div>    
</footer>

    
<script src="/js/jquery.js"></script>

    
<script src="/js/toki.js"></script>
  
  <script>hljs.initHighlightingOnLoad();</script>
</body>
</html>